//
// (C) Copyright 2009 Irantha Suwandarathna (irantha@gmail.com)
// All rights reserved.
//

/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using EffiProz.Core.Lib;
using EffiProz.Core.DataTypes;
using EffiProz.Core.Indexes;
using EffiProz.Core.Errors;
using EffiProz.Core.Persist;


namespace EffiProz.Core
{
    public class SetFunction
    {
        private HashSet distinctValues;
        private bool isDistinct;

        //
        private int setType;
        private int dataType;
        private SqlType type;

        //
        private int count;

        //
        private bool hasNull;
        private bool every = true;
        private bool some = false;
        private long currentLong;
        private double currentDouble;
        private decimal currentBigDecimal;
        private Object currentValue;

        public SetFunction(int setType, SqlType type, bool isDistinct)
        {

            this.setType = setType;
            this.type = type;

            if (isDistinct)
            {
                this.isDistinct = true;
                distinctValues = new HashSet();
            }

            if (setType == OpTypes.VAR_SAMP || setType == OpTypes.STDDEV_SAMP)
            {
                this.sample = true;
            }

            if (type != null)
            {
                dataType = type.typeCode;

                if (type.isIntervalType())
                {
                    dataType = Types.SQL_INTERVAL;
                }
            }
        }

        public void add(Session session, Object item)
        {

            if (item == null)
            {
                hasNull = true;

                return;
            }

            if (isDistinct && !distinctValues.add(item))
            {
                return;
            }

            count++;

            switch (setType)
            {

                case OpTypes.COUNT:
                    return;

                case OpTypes.AVG:
                case OpTypes.SUM:
                    {
                        switch (dataType)
                        {

                            case Types.TINYINT:
                            case Types.SQL_SMALLINT:
                            case Types.SQL_INTEGER:
                                currentLong += Convert.ToInt32(item);

                                return;

                            case Types.SQL_INTERVAL:
                                if (item is IntervalSecondData)
                                {
                                    currentLong += (((IntervalSecondData)item).units);

                                    currentLong += ((IntervalSecondData)item).nanos;

                                    if (Math.Abs(currentLong)
                                            >= DTIType.nanoScaleFactors[0])
                                    {
                                        currentLong += (currentLong
                                        / DTIType.nanoScaleFactors[0]);

                                        currentLong %= DTIType.nanoScaleFactors[0];
                                    }
                                }
                                else if (item is IntervalMonthData)
                                {
                                    currentLong += (((IntervalMonthData)item).units);
                                }

                                return;

                            case Types.SQL_BIGINT:
                                currentLong += Convert.ToInt64(item);

                                return;

                            case Types.SQL_REAL:
                            case Types.SQL_FLOAT:
                            case Types.SQL_DOUBLE:
                                currentDouble += Convert.ToDouble(item);

                                return;

                            case Types.SQL_NUMERIC:
                            case Types.SQL_DECIMAL:

                                currentBigDecimal += Convert.ToDecimal(item);
                                return;

                            default:
                                throw Error.error(ErrorCode.X_42565);
                        }
                    }
                case OpTypes.MIN:
                    {
                        if (currentValue == null)
                        {
                            currentValue = item;

                            return;
                        }

                        if (type.compare(session, currentValue, item) > 0)
                        {
                            currentValue = item;
                        }

                        return;
                    }
                case OpTypes.MAX:
                    {
                        if (currentValue == null)
                        {
                            currentValue = item;

                            return;
                        }

                        if (type.compare(session, currentValue, item) < 0)
                        {
                            currentValue = item;
                        }

                        return;
                    }
                case OpTypes.EVERY:
                    if (!(item is Boolean))
                    {
                        throw Error.error(ErrorCode.X_42565);
                    }

                    every = every && Convert.ToBoolean(item);

                    return;

                case OpTypes.SOME:
                    if (!(item is Boolean))
                    {
                        throw Error.error(ErrorCode.X_42565);
                    }

                    some = some || Convert.ToBoolean(item);

                    return;

                case OpTypes.STDDEV_POP:
                case OpTypes.STDDEV_SAMP:
                case OpTypes.VAR_POP:
                case OpTypes.VAR_SAMP:
                    addDataPoint((ValueType)item);

                    return;

                default:
                    throw Error.runtimeError(ErrorCode.U_S0500, "SetFunction");
            }
        }

        public Object getValue(Session session)
        {

            if (hasNull)
            {
                session.addWarning(Error.error(ErrorCode.W_01003));
            }

            if (setType == OpTypes.COUNT)
            {
                return (count);
            }

            if (count == 0)
            {
                return null;
            }

            switch (setType)
            {

                case OpTypes.AVG:
                    {
                        switch (dataType)
                        {

                            case Types.TINYINT:
                            case Types.SQL_SMALLINT:
                            case Types.SQL_INTEGER:
                                return (long)(currentLong / count);

                            case Types.SQL_BIGINT:
                                {
                                    long value = (long)(currentLong / count);

                                    return (value);
                                }
                            case Types.SQL_REAL:
                            case Types.SQL_FLOAT:
                            case Types.SQL_DOUBLE:
                                return (double)(currentDouble / count);

                            case Types.SQL_NUMERIC:
                            case Types.SQL_DECIMAL:
                                return (decimal)(currentBigDecimal / count);

                            case Types.SQL_INTERVAL:
                                {
                                    long bi =
                                        (long)(currentLong / count);

                                    if (!NumberType.isInLongLimits(bi))
                                    {
                                        throw Error.error(ErrorCode.X_22015);
                                    }

                                    if (((IntervalType)type).isDaySecondIntervalType())
                                    {
                                        return new IntervalSecondData(bi,
                                                                      currentLong,
                                                                      (IntervalType)type,
                                                                      true);
                                    }
                                    else
                                    {
                                        return IntervalMonthData.newIntervalMonth(
                                            bi, (IntervalType)type);
                                    }
                                }
                            default:
                                throw Error.runtimeError(ErrorCode.U_S0500,
                                                         "SetFunction");
                        }
                    }
                case OpTypes.SUM:
                    {
                        switch (dataType)
                        {

                            case Types.TINYINT:
                            case Types.SQL_SMALLINT:
                            case Types.SQL_INTEGER:
                                return (currentLong);

                            case Types.SQL_BIGINT:
                                return (currentLong);

                            case Types.SQL_REAL:
                            case Types.SQL_FLOAT:
                            case Types.SQL_DOUBLE:
                                return (currentDouble);

                            case Types.SQL_NUMERIC:
                            case Types.SQL_DECIMAL:
                                return currentBigDecimal;

                            case Types.SQL_INTERVAL:
                                {
                                    long bi = currentLong;

                                    if (!NumberType.isInLongLimits(bi))
                                    {
                                        throw Error.error(ErrorCode.X_22015);
                                    }

                                    if (((IntervalType)type).isDaySecondIntervalType())
                                    {
                                        return new IntervalSecondData(bi,
                                                                      currentLong,
                                                                      (IntervalType)type,
                                                                      true);
                                    }
                                    else
                                    {
                                        return IntervalMonthData.newIntervalMonth(
                                            bi, (IntervalType)type);
                                    }
                                }
                            default:
                                throw Error.runtimeError(ErrorCode.U_S0500,
                                                         "SetFunction");
                        }
                    }
                case OpTypes.MIN:
                case OpTypes.MAX:
                    return currentValue;

                case OpTypes.EVERY:
                    return every ? true
                                 : false;

                case OpTypes.SOME:
                    return some ? true
                                : false;

                case OpTypes.STDDEV_POP:
                case OpTypes.STDDEV_SAMP:
                    return getStdDev();

                case OpTypes.VAR_POP:
                case OpTypes.VAR_SAMP:
                    return getVariance();

                default:
                    throw Error.runtimeError(ErrorCode.U_S0500, "SetFunction");
            }
        }

        /**
         * During parsing and before an instance of SetFunction is created,
         * getType is called with type parameter set to correct type when main
         * SELECT statements contain aggregates.
         *
         */
        public static SqlType getType(int setType, SqlType type)
        {

            if (setType == OpTypes.COUNT)
            {
                return SqlType.SQL_INTEGER;
            }

            int dataType = type.isIntervalType() ? Types.SQL_INTERVAL
                                                 : type.typeCode;

            switch (setType)
            {

                case OpTypes.AVG:
                    {
                        switch (dataType)
                        {

                            case Types.TINYINT:
                            case Types.SQL_SMALLINT:
                            case Types.SQL_INTEGER:
                            case Types.SQL_BIGINT:
                            case Types.SQL_REAL:
                            case Types.SQL_FLOAT:
                            case Types.SQL_DOUBLE:
                            case Types.SQL_NUMERIC:
                            case Types.SQL_DECIMAL:
                            case Types.SQL_INTERVAL:
                                return type;

                            default:
                                throw Error.error(ErrorCode.X_42565);
                        }
                    }
                case OpTypes.SUM:
                    {
                        switch (dataType)
                        {

                            case Types.TINYINT:
                            case Types.SQL_SMALLINT:
                            case Types.SQL_INTEGER:
                                return SqlType.SQL_BIGINT;

                            case Types.SQL_BIGINT:
                                return SqlType.SQL_DECIMAL_BIGINT_SQR;

                            case Types.SQL_REAL:
                            case Types.SQL_FLOAT:
                            case Types.SQL_DOUBLE:
                                return SqlType.SQL_DOUBLE;

                            case Types.SQL_NUMERIC:
                            case Types.SQL_DECIMAL:
                                return SqlType.getType(type.typeCode, 0,
                                                    type.precision * 2, type.scale);

                            case Types.SQL_INTERVAL:
                                return IntervalType.newIntervalType(
                                    type.typeCode, DTIType.maxIntervalPrecision,
                                    type.scale);

                            default:
                                throw Error.error(ErrorCode.X_42565);
                        }
                    }
                case OpTypes.MIN:
                case OpTypes.MAX:
                    return type;

                case OpTypes.EVERY:
                case OpTypes.SOME:
                    if (type.isBooleanType())
                    {
                        return SqlType.SQL_BOOLEAN;
                    }
                    break;

                case OpTypes.STDDEV_POP:
                case OpTypes.STDDEV_SAMP:
                case OpTypes.VAR_POP:
                case OpTypes.VAR_SAMP:
                    if (type.isNumberType())
                    {
                        return SqlType.SQL_DOUBLE;
                    }
                    break;

                default:
                    throw Error.runtimeError(ErrorCode.U_S0500, "SetFunction");
            }

            throw Error.error(ErrorCode.X_42565);
        }



        // end long sum
        // statistics support - written by Campbell
        // this section was orginally an independent class
        private double sk;
        private double vk;
        private long n;
        private bool initialized;
        private bool sample;

        private void addDataPoint(Object x)
        {    // optimized

            double xi;
            double xsi;
            long nm1;

            if (x == null)
            {
                return;
            }

            xi = Convert.ToDouble(x);

            if (!initialized)
            {
                n = 1;
                sk = xi;
                vk = 0.0;
                initialized = true;

                return;
            }

            n++;

            nm1 = (n - 1);
            xsi = (sk - (xi * nm1));
            vk += ((xsi * xsi) / n) / nm1;
            sk += xi;
        }

        private object getVariance()
        {

            if (!initialized)
            {
                return null;
            }

            return sample ? (n == 1) ? null    // NULL (not NaN) is correct in this case
                                     : (object)(double)(vk / (double)(n - 1))
                          : (object)(double)(vk / (double)(n));
        }

        private object getStdDev()
        {

            if (!initialized)
            {
                return null;
            }

            return sample ? (n == 1) ? null    // NULL (not NaN) is correct in this case
                                     : (object)(double)(Math.Sqrt(vk / (double)(n - 1)))
                          : (object)(double)(Math.Sqrt(vk / (double)(n)));
        }

        // end statistics support

    }
}
